﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Web;
using System.Windows;
using Lime49;
using LockCrypt.Core;
using Newtonsoft.Json;
using System.IO;
using Newtonsoft.Json.Linq;
using LockCrypt.DAL;

namespace LockCrypt.Firefox {
    public class FirefoxServer : INotifyPropertyChanged {
        private static FirefoxServer _instance = new FirefoxServer();
        private ILogProvider _logger;

        private static Dictionary<int, Socket> SocketMap = new Dictionary<int, Socket>();
        private int port = 1556;
        private IPAddress listenAddress = IPAddress.Any;
        private TcpListener listener;
        private int _requestTimeout = 30000;

        /// <summary>
        /// The template used for new accounts added from Firefox
        /// </summary>
        private readonly ITemplate _websiteTemplate = new TemplateBase() {
            Name = "URL",
            ID = "_pass",
        };

        #region Events
        /// <summary>
        /// Occurs when a client connects.
        /// </summary>
        public event EventHandler ClientConnected;

        /// <summary>
        /// Occurs when after each segment of a request has been received, before parsing or processing.
        /// </summary>
        public event EventHandler RequestReceived;

        /// <summary>
        /// Occurs after each segment of a request has been parsed.
        /// </summary>
        public event EventHandler RequestParsed;

        /// <summary>
        /// Occurs after the request has been processed.
        /// </summary>
        public event EventHandler RequestProcessed;

        /// <summary>
        /// Occurs after the connection to the client has been closed.
        /// </summary>
        public event EventHandler EndRequest;

        /// <summary>
        /// Occurs when new accounts are added.
        /// </summary>
        public delegate void AccountAddedDelegate(object sender, IEnumerable<IAccount> accounts);
        public event AccountAddedDelegate AccountAdded;
        #endregion

        /// <summary>
        /// Gets the FirefoxServer instance used to serve requests.
        /// </summary>
        /// <value>The FirefoxServer instance.</value>
        public static FirefoxServer Instance { get { return _instance; } }

            /// <summary>
        /// Gets or sets whether the server is started.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if the server is started, otherwise <c>false</c>.
        /// </value>
        public bool IsStarted {
            get { return listener.Server != null && listener.Server.IsBound; }
            set {
                if(value) { // start 
                    if(listener.Server == null || !listener.Server.IsBound) { // not listening
                        Start();
                    }
                } else {
                    if(listener != null && listener.Server != null && !listener.Server.IsBound) { // listening
                        Stop();
                    }
                }
                OnPropertyChanged("IsStarted");
            }
        }

        /// <summary>
        /// Gets or sets the logger to which to write.
        /// </summary>
        /// <value>The logger to which to write.</value>
        public ILogProvider Logger {
            get { return _logger; }
            set {
                _logger = value;
                OnPropertyChanged("Logger");
            }
        }

        public int RequestTimeout {
            get { return _requestTimeout; }
            set {
                _requestTimeout = value;
                OnPropertyChanged("RequestTimeout");
            }
        }

        /// <summary>
        /// Initializes, but does not start a new <see cref="FirefoxServer"/>.
        /// </summary>
        public FirefoxServer() {
            Logger = new NullLogger();
            try {
                this.listener = new TcpListener(listenAddress, port);
            } catch(Exception ex) {
                Logger.Log(ex.Message, true);
            }
        }

        /// <summary>
        /// Starts the server.
        /// </summary>
        public void Start() {
            if(port < 1 || port > 65536)
                throw new LockCryptException("Port must be between 1 and 65536", 200);
            try {
                listener = new TcpListener(listenAddress, port);
                listener.Start();
                Logger.Log(string.Format("Server started, listening {0}:{1}", listenAddress == IPAddress.Any ? "localhost" : listenAddress.ToString(), port));
                listener.BeginAcceptTcpClient(OnAccept, null);
            } catch(Exception ex) {
                Logger.Log(string.Format("Error starting Firefox server: {0}", ex.Message));
            }
        }

        /// <summary>
        /// Stops the server.
        /// </summary>
        public void Stop() {
            Logger.Log(new StatusMessage("Stopping server...", StatusMessage.StatusMessageType.Status, true));
            try {
                listener.Stop();
                Logger.Log("Stopped");
            } catch(Exception ex) {
                Logger.Log(string.Format("Error stopping Firefox server: {0}", ex.Message));
            }
        }

        /// <summary>
        /// Starts receiving data Asynchronously when a request is received.
        /// </summary>
        /// <param name="ar">The data received.</param>
        private void OnAccept(IAsyncResult ar) {
            try {
                if(IsStarted) {
                    listener.BeginAcceptTcpClient(OnAccept, null);
                    TcpClient client = listener.EndAcceptTcpClient(ar);
                    Logger.Log(string.Format("{0} connected", client.Client.RemoteEndPoint));
                    if(ClientConnected != null)
                        ClientConnected(this, EventArgs.Empty);
                    BackgroundWorker worker = new BackgroundWorker();
                    worker.DoWork += (s, dwe) => {
                        RequestContext ctx = new RequestContext(client.Client);
                        SocketMap[System.Threading.Thread.CurrentThread.ManagedThreadId] = client.Client;
                        int startTickCount = Environment.TickCount;
                        int bytesRead = 0;
                        try {
                            do {
                                if(Environment.TickCount > startTickCount + RequestTimeout)
                                    throw new FirefoxException("Request Timeout", 408);
                                try {
                                    // read into buffer
                                    bytesRead = ctx.Socket.Receive(ctx.RequestBuffer.Buffer, 0, ctx.RequestBuffer.Buffer.Length, SocketFlags.None);
                                    if(bytesRead > 0) {
                                        ctx.RequestBuffer.RequestBuilder.Append(Encoding.UTF8.GetString(ctx.RequestBuffer.Buffer, 0, bytesRead));
                                    }
                                    if(RequestReceived != null)
                                        RequestReceived(this, EventArgs.Empty);
                                    // parse buffer contents
                                    Parse(ctx);
                                    if(RequestParsed != null)
                                        RequestParsed(this, EventArgs.Empty);
                                } catch(SocketException ex) {
                                    if(ex.SocketErrorCode == SocketError.WouldBlock || ex.SocketErrorCode == SocketError.IOPending || ex.SocketErrorCode == SocketError.NoBufferSpaceAvailable) {
                                        Thread.Sleep(30); // wait for more data
                                    } else {
                                        throw;
                                    }
                                }
                            } while(!ctx.RequestBuffer.ReceiveFinished);//bytesRead > 0 && 
                            ProcessRequest(ctx);
                        } catch(FirefoxException hEx) {
                            TerminateClient(ctx, hEx);
                        }
                    };
                    worker.RunWorkerAsync();
                }
            } catch(Exception ex) {
                Logger.Log("Socket error: " + ex.Message, true);
            }
        }

        /// <summary>
        /// The context containing the request to parse.
        /// </summary>
        /// <param name="context">The context.</param>
        public void Parse(RequestContext context) {
            if(context == null || context.RequestBuffer == null)
                throw new ArgumentNullException("context", "Request cannot be null");
            int lastParsed = context.RequestBuffer.Position,
                requestLength = context.RequestBuffer.RequestBuilder.Length;
            if(lastParsed < requestLength - 1) {
                string currentContents = context.RequestBuffer.RequestBuilder.ToString();
                if(currentContents.Length > 0 && currentContents[currentContents.Length - 1] == (char)23) { // the ETB char was a (slightly) bizarre choice, but kept for compatibility.
                    context.RequestBuffer.RequestBuilder.Replace((char)23, ' ');
                    context.RequestBuffer.ReceiveFinished = true;
                    Logger.Log(string.Format("<-[{0}]-", context.RemoteIP) + context.RequestBuffer.RequestBuilder.ToString(), StatusMessage.StatusMessageType.Style1);
                }
            }
        }

        /// <summary>
        /// Processes a received request.
        /// </summary>
        /// <param name="context">The context.</param>
        private void ProcessRequest(RequestContext context) {
            try {
                JsonSerializer serializer = new JsonSerializer();
                serializer.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
                FirefoxRequest request = JsonConvert.DeserializeObject<FirefoxRequest>(context.RequestBuffer.RequestBuilder.ToString());
                string response = string.Empty;
                try {
                    switch(request.Type) {
                        case RequestType.CheckIfExists:
                            response = FindExactMatch(request);
                            break;
                        case RequestType.NameEnquiry:
                            response = FindAccountNames(request);
                            break;
                        case RequestType.Ping:
                            response = "PONG";
                            break;
                        case RequestType.Request:
                            response = FindAccounts(request);
                            break;
                        case RequestType.Save:
                            AddAccount(request);
                            break;
                        case RequestType.Import:
                            response = ImportFromFirefox(context.RequestBuffer.RequestBuilder.ToString());
                            break;
                        case RequestType.Export:
                            response = ExportToFirefox();
                            break;
                    }
                } catch(Exception ex) {
                    response = "{\"error\":\"" + ex.Message + "\"}";
                }
                Logger.Log(string.Format("  -[{0}]->", context.RemoteIP) + response, StatusMessage.StatusMessageType.Style2);
                context.Socket.Send(Encoding.UTF8.GetBytes(response));
            } catch(FirefoxException fEx) {
                TerminateClient(context, fEx);
            } catch(Exception ex) {
                TerminateClient(context, new FirefoxException(ex.Message, ex.InnerException, 500));
                Logger.Log(ex.Message, true);
            }
            if(RequestProcessed != null)
                RequestProcessed(this, EventArgs.Empty);
            context.Socket.Close();
            if(EndRequest != null)
                EndRequest(this, EventArgs.Empty);
        }

        #region Processing Methods
        /// <summary>
        /// Finds the exact match.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns>A JSON string with an 'exists' or 'error' property</returns>
        private string FindExactMatch(FirefoxRequest request) {
            JObject jsoResponse = new JObject();
            try {
                if(!DalConfigurationManager.Provider.IsConnected) {
                    jsoResponse.Add("error", "Not logged in to LockCrypt");
                } else {
                    IEnumerable<IAccount> accounts = DalConfigurationManager.Provider.GetAccounts().Where(a => a.Template.ID == "_pass");
                    bool exists = false;
                    string url = stripURL(new Uri(request.URL), false);
                    IEnumerable<FirefoxRequest.RequestField> formFields = request.Fields;
                    foreach(IAccount account in accounts) {
                        IField urlField = account.Fields.FirstOrDefault(f => f.Name == "URL");
                        if(urlField == null || !urlField.Value.Contains(url))
                            continue;
                        // account exists for this URL
                        try {
                            if(account.Fields.Count != request.Fields.Count) {
                                //one field for the URL, if the field count is different, they can't be identical
                                continue;
                            }
                            bool accountIdentical = true;
                            foreach(var submittedField in formFields) {
                                // check to see if there's a field in the form which is different from the saved account
                                string fieldName = HttpUtility.UrlDecode(submittedField.Name);
                                string fieldValue = HttpUtility.UrlDecode(submittedField.Value);
                                IField fieldInAccount = account.Fields.FirstOrDefault(f => f.Name.Equals(fieldName, StringComparison.InvariantCultureIgnoreCase));
                                if(fieldInAccount != null && fieldInAccount.Value != fieldValue) {
                                    accountIdentical = false;
                                }
                            }
                            if(accountIdentical) {
                                exists = true;
                                break;
                            }
                        } catch(Exception ex) {
                            jsoResponse.Add("error", ex.Message);
                        }
                    }
                    jsoResponse.Add("exists", exists);
                }
            } catch(Exception ex) {
                try {
                    jsoResponse.Add("error", ex.Message);
                } catch {
                }
            }
            return jsoResponse.ToString();
        }

        /// <summary>
        /// Finds the names of existing accounts  for a URL.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns>A JSON array of account names</returns>
        /// <example>
        /// 
        /// </example>
        private string FindAccountNames(FirefoxRequest request) {
            IEnumerable<IAccount> accounts = DalConfigurationManager.Provider.GetAccounts().Where(a => a.Template.ID == "_pass");
            JObject jso = new JObject();
            try {
                Uri currentUrl = new Uri(request.URL);
                if(!DalConfigurationManager.Provider.IsConnected) {
                    jso.Add("error", "Not logged in to LockCrypt");
                } else {
                    string name = Utils.GetNextAvailableName(stripURL(currentUrl, true), accounts.Select(a => a.Name));
                    jso.Add("suggested", HttpUtility.UrlEncode(name).Replace('+', ' '));
                    JArray existingNames = new JArray();
                    foreach(IAccount account in accounts) {
                        try {
                            IField urlField = account.Fields.FirstOrDefault(f => f.Name == "URL");
                            if(urlField == null)
                                continue;
                            if(string.IsNullOrEmpty(urlField.Value)) {
                                existingNames.Add(HttpUtility.UrlEncode(account.Name).Replace('+', ' '));
                            }
                        } catch {
                            //todo: balloon tip
                            _logger.Log("Account has been modified since it was saved", true);
                        }
                    }
                    jso.Add("existing", existingNames);
                }
            } catch(Exception ex) {
                _logger.Log(ex.Message, true);
            }
            return jso.ToString();
        }

        /// <summary>
        /// Finds accounts matching the current URL.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns>A JSON object containing accounts matching the request URL.</returns>
        private string FindAccounts(FirefoxRequest request) {
            IEnumerable<IAccount> accounts = DalConfigurationManager.Provider.GetAccounts().Where(a => a.Template.ID == "_pass");
            JObject jsoMatches = new JObject();
            try {
                if(!DalConfigurationManager.Provider.IsConnected) {
                    jsoMatches.Add("error", "Not logged in to LockCrypt");
                } else {
                    JArray jsaAccounts = new JArray(),
                           jsaFields;
                    JObject jsoCurrentAccount,
                            jsoCurrentField;
                    foreach(IAccount account in accounts) {
                        IField urlField = account.Fields.FirstOrDefault(f => f.Name == "URL");
                        if(urlField == null)
                            continue;
                        if(string.IsNullOrEmpty(urlField.Value) || new WildcardUrl(urlField.Value).Matches(request.URL)) {
                            jsoCurrentAccount = new JObject();
                            jsoCurrentAccount.Add("name", HttpUtility.UrlEncode(account.Name).Replace('+', ' '));
                            if(account.Fields.Any(f=>f.Name == "Username Field")) {
                                try {
                                    // account has either been saved automatically or exported from Firefox
                                    jsoCurrentAccount.Add("username", HttpUtility.UrlEncode(account.Fields.FirstOrDefault(f => f.Name == "Username").Value).Replace('+', ' '));
                                    jsoCurrentAccount.Add("usernameID", HttpUtility.UrlEncode(account.Fields.FirstOrDefault(f => f.Name == "Username Field").Value).Replace('+', ' '));
                                    jsoCurrentAccount.Add("password", HttpUtility.UrlEncode(account.Fields.FirstOrDefault(f => f.Name == "Password").Value).Replace('+', ' '));
                                    jsoCurrentAccount.Add("passwordID", HttpUtility.UrlEncode(account.Fields.FirstOrDefault(f => f.Name == "Password Field").Value).Replace('+', ' '));
                                } catch {
                                    //todo: balloon tip
                                    _logger.Log("Account has been modified since it was saved", true);
                                }
                            } else {
                                jsaFields = new JArray();
                                foreach(IField field in account.Fields) {
                                    jsoCurrentField = new JObject();
                                    jsoCurrentField.Add("name", HttpUtility.UrlEncode(field.Name).Replace('+', ' '));
                                    jsoCurrentField.Add("value", HttpUtility.UrlEncode(field.Value).Replace('+', ' '));
                                    jsaFields.Add(jsoCurrentField);
                                }
                                jsoCurrentAccount.Add("fields", jsaFields);
                            }
                            jsaAccounts.Add(jsoCurrentAccount);
                        }
                    }
                    jsoMatches.Add("matches", jsaAccounts);
                }
            } catch(Exception ex) {
                _logger.Log(ex.Message, true);
                try {
                    jsoMatches.Add("error", "Could not parse account");
                } catch {
                }
            }
            return jsoMatches.ToString();
        }

        /// <summary>
        /// Saves a new account.
        /// </summary>
        /// <param name="request">The request.</param>
        private void AddAccount(FirefoxRequest request) {
            IAccount acc = new AccountBase() {
                Name = HttpUtility.UrlDecode(request.Name),
                Icon = "network",
                Template = _websiteTemplate
            };
            int sortIndex = -1;
            acc.Fields.Add(new FieldBase() {
                Name = "URL",
                Value = HttpUtility.UrlDecode(request.URL),
                Meta = FieldMetaData.Url,
                SortIndex = ++sortIndex
            });
            var fieldsToInclude = request.Fields.Where(f=>f.Include);
            foreach(FirefoxRequest.RequestField submittedField in fieldsToInclude) {
                IField field = new FieldBase() {
                    Name = HttpUtility.UrlDecode(submittedField.Name),
                    Value = HttpUtility.UrlDecode(submittedField.Value),
                    Meta = submittedField.Name.ToLowerInvariant().Contains("pass") ? FieldMetaData.Password : FieldMetaData.None,
                    SortIndex = ++sortIndex
                };
                acc.Fields.Add(field);
            }
            var accounts = new[] { acc };
            DalConfigurationManager.Provider.AddAccounts(accounts, Logger);
            if(AccountAdded != null)
                AccountAdded(this, accounts);
        }

        /// <summary>
        /// Imports passwords from Firefox.
        /// </summary>
        /// <param name="rawData">The data from Firefox.</param>
        /// <returns>A JSON string, contaning "error" or "response"</returns>
        public String ImportFromFirefox(string rawData) {
            List<string> errors = new List<string>();
            JObject jsoResponse = new JObject();
            int numExported = 0;
            try {
                if(!DalConfigurationManager.Provider.IsConnected) {
                    jsoResponse.Add("error", "Not logged in to LockCrypt");
                } else {
                    IEnumerable<IAccount> accounts = DalConfigurationManager.Provider.GetAccounts().Where(a => a.Template.ID == "_pass");
                    IEnumerable<IGroup> groups = DalConfigurationManager.Provider.GetGroups();
                    FirefoxImportData data = JsonConvert.DeserializeObject<FirefoxImportData>(rawData);

                    IGroup firefoxGroup = groups.FirstOrDefault(g => g.Name.Equals("Firefox Passwords", StringComparison.InvariantCultureIgnoreCase));
                    if(firefoxGroup == null) {
                        firefoxGroup = new Group()
                                           {
                                               Name = "Firefox Passwords"
                                           };
                        DalConfigurationManager.Provider.AddGroups(new[] {firefoxGroup}, _logger);
                    }

                    List<string> existingNames = accounts.Select(a => a.Name).ToList(); // don't duplicate account names for the same URL.
                    List<IAccount> newAccounts = new List<IAccount>();
                    foreach(FirefoxImportData.FirefoxImportAccount acc in data.Accounts) {
                        string url = HttpUtility.UrlDecode(acc.Url),
                               username = HttpUtility.UrlDecode(acc.UserName);
                        bool accountExists = false;
                        foreach(IAccount existingAccount in accounts) {
                            IField urlField = existingAccount.Fields.FirstOrDefault(f => f.Name == "URL");
                            if(urlField == null)
                                continue;
                            if(url == urlField.Value) {
                                IField usernameField = existingAccount.Fields.FirstOrDefault(f => f.Name == "Username");
                                if(usernameField != null && usernameField.Value == username) {
                                    accountExists = true;
                                    break;
                                }
                            }
                        }

                        if(!accountExists) {
                            string newName = Utils.GetNextAvailableName(stripURL(new Uri(url), true), existingNames);
                            IAccount newAccount = new AccountBase()
                                                      {
                                                          Template = _websiteTemplate,
                                                          Name = newName,
                                                          ParentID = firefoxGroup.ID,
                                                          Icon = "network"
                                                      };
                            int sortIndex = -1;
                            newAccount.Fields.Add(new FieldBase()
                                                      {
                                                          Name = "URL",
                                                          Value = HttpUtility.UrlDecode(acc.Url),
                                                          Meta = FieldMetaData.Url,
                                                          SortIndex = ++sortIndex
                                                      });
                            newAccount.Fields.Add(new FieldBase()
                                                      {
                                                          Name = "Username",
                                                          Value = HttpUtility.UrlDecode(acc.UserName),
                                                          Meta = FieldMetaData.None,
                                                          SortIndex = ++sortIndex
                                                      });
                            newAccount.Fields.Add(new FieldBase()
                                                      {
                                                          Name = "Username Field",
                                                          Value = HttpUtility.UrlDecode(acc.UserNameID),
                                                          Meta = FieldMetaData.None,
                                                          SortIndex = ++sortIndex
                                                      });
                            newAccount.Fields.Add(new FieldBase()
                                                      {
                                                          Name = "Password",
                                                          Value = HttpUtility.UrlDecode(acc.Password),
                                                          Meta = FieldMetaData.Password,
                                                          SortIndex = ++sortIndex
                                                      });
                            newAccount.Fields.Add(new FieldBase()
                                                      {
                                                          Name = "Password Field",
                                                          Value = HttpUtility.UrlDecode(acc.PasswordID),
                                                          Meta = FieldMetaData.None,
                                                          SortIndex = ++sortIndex
                                                      });
                            newAccounts.Add(newAccount);
                            numExported++;
                        }
                    }
                    DalConfigurationManager.Provider.AddAccounts(newAccounts, _logger);
                    if(AccountAdded != null)
                        AccountAdded(this, accounts);
                    string msg = I18nUtils.GetString("Strings", "ExportedNumFmt", numExported.ToString());
                    _logger.Log(msg, StatusMessage.StatusMessageType.Success);
                    jsoResponse.Add("response", HttpUtility.UrlEncode(msg).Replace('+', ' '));
                }
            } catch(Exception ex) {
                _logger.Log(ex.Message, StatusMessage.StatusMessageType.Error);
                errors.Add(ex.Message + "\n");
            }
            if(errors.Any()) {
                jsoResponse.Add("error", errors.ToString());
            }
            return jsoResponse.ToString();
        }

        public String ExportToFirefox() {
            List<string> errors = new List<string>();
            JObject jsoResponse = new JObject();
            try {
                if(!DalConfigurationManager.Provider.IsConnected) {
                    jsoResponse.Add("error", "Not logged in to LockCrypt");
                } else {
                    IEnumerable<IAccount> accounts = DalConfigurationManager.Provider.GetAccounts().Where(a => a.Template.ID == "_pass");
                    List<FirefoxExportAccount> accountsToExport = new List<FirefoxExportAccount>();
                    String hostname;
                    foreach(IAccount account in accounts) {
                        IField urlField = account.Fields.FirstOrDefault(f => f.Name == "URL");
                        if(urlField == null)
                            continue;
                        try {
                            hostname = stripURL(new Uri(urlField.Value), false);
                            if(hostname.Length > 10) {
                                // more than http://a.b
                                IField usernameField = account.Fields.FirstOrDefault(f => f.Name == "Username"),
                                       passwordField = account.Fields.FirstOrDefault(f => f.Name == "Password");
                                if(usernameField == null || passwordField == null)
                                    continue;
                                FirefoxExportAccount acc = new FirefoxExportAccount() {
                                    hostname = HttpUtility.UrlEncode(hostname).Replace('+', ' '),
                                    username = HttpUtility.UrlEncode(usernameField.Value).Replace('+', ' '),
                                    usernameField = HttpUtility.UrlEncode(account.Fields.FirstOrDefault(f => f.Name == "Username Field").Value).Replace('+', ' '),
                                    password = HttpUtility.UrlEncode(passwordField.Value).Replace('+', ' '),
                                    passwordField = HttpUtility.UrlEncode(account.Fields.FirstOrDefault(f => f.Name == "Password Field").Value).Replace('+', ' ')
                                };
                                accountsToExport.Add(acc);
                            }
                        } catch(Exception ex) {
                            _logger.Log("Invalid account: " + ex.Message, true);
                        }
                    }
                    jsoResponse.Add("accounts", JToken.FromObject(accountsToExport));// JsonConvert.SerializeObject(accountsToExport, Formatting.None));
                }
            } catch(Exception ex) {
                _logger.Log(ex.Message, StatusMessage.StatusMessageType.Error);
                errors.Add(ex.Message + "\n");
            }
            if(errors.Any()) {
                jsoResponse.Add("error", errors.ToString());
            }
            return jsoResponse.ToString();
        }

        #endregion

        /// <summary>
        /// Terminates the connection to client with an error code.
        /// </summary>
        /// <param name="context">The HTTP context.</param>
        /// <param name="fEx">The HTTP exception to send.</param>
        private void TerminateClient(RequestContext context, FirefoxException fEx) {
            try {
                string response = "Connection: Close\r\n";
                _logger.Log(string.Format("      -[{0}]->", context.RemoteIP) + response, StatusMessage.StatusMessageType.Style2);
                context.Socket.Send(Encoding.UTF8.GetBytes(response));
                context.Socket.Close();
                _logger.Log("Connection terminated: " + fEx.Message, true);
            } catch(Exception ex) {
                _logger.Log("Socket error: " + ex.Message, true);
            }
        }

        //todo: fix this (it was ripped from LC Java)
        public static string stripURL(Uri url, bool makePretty) {
            StringBuilder ret = new StringBuilder();
            try {
                string protocol = url.Scheme.ToLowerInvariant();
                string host = url.Host;
                if(makePretty) {
                    if(url.IsFile) {
                        int questionMark = url.LocalPath.IndexOf("?");
                        string filePath = questionMark >= 0 && questionMark < url.LocalPath.Length - 1 ?
                                          url.LocalPath.Substring(0, questionMark)
                                          : url.LocalPath;
                        string fileName = Path.GetFileName(filePath);
                            // query string exists and there are chars after the '?'
                        int lastSlash = fileName.LastIndexOf("/") + 1;
                        if(lastSlash == fileName.Length) {
                            ret.Append(fileName);
                        } else {
                            ret.Append(fileName.Substring(lastSlash));
                        }
                    } else {
                        if(host.StartsWith("www.")) {
                            ret.Append(host.Substring(4, 1).ToUpper())
                                .Append(host.Substring(5));
                        } else {
                            ret.Append(host.Substring(0, 1).ToUpper())
                                .Append(host.Substring(1));
                        }
                    }
                } else {
                    ret.Append(protocol).Append("://");
                    if(url.IsFile) {
                    //if(protocol == "file") {
                        //ret.Append(url.getPath());
                        ret.Append(url.PathAndQuery);
                    } else {
                        ret.Append(url.Host);
                    }
                }
            } catch {
                ret.Append(url);
            }

            return ret.ToString();
        }

        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(string strPropertyName)
        {
            if(PropertyChanged != null) {
                PropertyChanged(this, new PropertyChangedEventArgs(strPropertyName));
            }
        }
    }
}
